home *** CD-ROM | disk | FTP | other *** search
Text File | 1995-07-30 | 16.1 KB | 397 lines | [TEXT/ttxt] |
- Gray-scale image processing library
-
- ***** For the version history, read on
-
- ***** You'll need libserv.a, an "advanced" C++ iostream classlib,
- to compile and use this grayimage library
-
- That iostream code has been posted on comp.sources.misc and info-mac
- (info-mac:/dev/lib/advanced-io-cpp.hqx) and is also available from
- ftp://replicant.csci.unt.edu/pub/oleg/c++advio.shar
- ftp://replicant.csci.unt.edu/pub/oleg/c++advio.cpt.hqx (Mac version)
- The Mac version is identical to the UNIX version, but includes CW projects
- and a compiled library (for a PowerMac).
-
- ***** Verification files: vimage vrectangle vimage_io vfilter
- vmorph_filter vfractal_map
-
- Don't forget to compile and run them, see comments in the Makefile for
- details. The verification code checks to see that all the functions
- in this package have compiled and run well. The code can also serve as
- an example how package classes/functions can be used. Sample test
- pictures are not distributed with the .shar archive to save space. So,
- to run vimage_io.cc, you need to make a subdirectory 'pictures' and
- put there any pgm/xwd/tiff file you'd like to be used in the test
- (name the file as 512.xwd though, or modify vimage_io.cc). At any
- rate, you can get a sample file from
- ftp://replicant.csci.unt.edu/pub/oleg/512.xwd.gz
-
- ***** Highlights and idioms
-
- Elementary pixel operations: assigning/subtracting/adding etc a value
- to all pixels, comparing every pixel with a value,
- assigning/adding/subtracting/comparing two images, computing image
- extrema, various norms and "scalar" products
-
- IMAGE im1(256,256,8); IMAGE im2(im1);
- im1 = 1; im2 = im1; im1 *= 4;
- assert( im1 == 4 ); assert( im1 > 0 );
- im1.invert(); im1.clear(); im2 <<= 2; im1 = 5;
- im1 &= 0xfe; assert( im1==im2 );
- im1.clear(); im1 -= im2; im2 = im1;
- assert( im1 * im1 == norm_2_sqr(im1) );
-
-
- Accessing square or rectangular parts of an image without much fuss
- (and without moving a lot of stuff around)
-
- im1 = 2*pattern; im2 = pattern;
- rowcol rightbottom(im1.q_nrows()-1,im1.q_ncols()-1);
- rowcol center(im1.q_nrows()/2,im1.q_ncols()/2);
- // Modifying the pixels only within
- // the lower left quadrant
- im1.rectangle(center,rightbottom) -= 2*pattern;
- assert( !(im1 == 2*pattern) && !(im1 != 2*pattern) );
-
-
- Image i/o: supports reading and writing PGM, XWD and Group G
- (grayscale) TIFF file formats with automatical recognition of the
- input image file format
- // Note the "extended" file name
- IMAGE raw_image("zcat ../pictures/lenna.tiff.Z |");
- IMAGE image = raw_image.square_of(256,rowcol(120,100));
- image.write_tiff("/tmp/ior","Original image");
- image.display("Image to display");
- image.write_pgm("| xv -"); // another way to display an image
-
- Squeezing/stretching and coercing images. Coercing means that one can
- assign one image to another no matter what their dimensions are. The
- source image would be shrunk/stretched to fit. Any dimension ratios
- are possible, absolutely any
-
- Test_image = pattern;
- IMAGE blown_out(IMAGE::Expand,Test_image);
- IMAGE blown_shrunk(IMAGE::Shrink,blown_out);
- assert( blown_shrunk == Test_image );
-
- IMAGE shrunk(Test_image.q_nrows()/3+1,
- Test_image.q_ncols()/2,Test_image.q_depth());
- shrunk.coerce(Test_image);
- IMAGE vert_stretched(Test_image.q_nrows()+7,
- Test_image.q_ncols(),Test_image.q_depth());
- vert_stretched.coerce(Test_image);
- assert( vert_stretched.rectangle(rowcol(0,0),
- rowcol(Test_image.q_nrows()-1,
- Test_image.q_ncols()-1))
- == Test_image );
-
- Note that the last operation involves an implicit conversion from a
- rectangle to an image
-
-
- Advanced pixel operations via PixelAction. This is the most natural
- (and efficient!) way of doing a "sweeping" image processing (that is,
- an operation that is going to involve any pixel in some systematic
- way). Thus, instead of writing a loop over all image rows and
- columns, one merely needs to specify what action is to be performed on
- a current pixel. The package would take care of the iteration, which
- is more efficient than a for() loop. The iteration walks through all
- the pixels in a row-by-row fashion; one can use this knowledge if one
- wishes to. PixelAction would tell you the location of the pixel being
- accessed/modified, while PixelPrimAction won't (if it doesn't
- matter). The latter is faster, of course.
-
- For example, the following snippet squares all image pixels and
- verifies that
-
- {
- IMAGE im(Test_image); IMAGE im1(im);
- im = pattern;
- im.square_of(size,rowcol(0,0)) = -1;
- im1 = im;
- struct SqrImage : public PixelPrimAction {
- void operation(GRAY& pixel) { pixel = sqr((GRAY_SIGNED)pixel); }
- };
- im1.apply(SqrImage());
- assert( sum_over((Rectangle)im1) == im.norm_2_sqr() );
- }
-
- Still, lookup tables are better for that purpose (see below). In the
- next example, which makes a pin-striped image, the location of the
- current pixel _is_ important:
-
- class MakeVStripes : public PixelAction
- {
- GRAY pattern;
- void operation(GRAY& pixel) { pixel = col & 1 ? pattern : 0; }
- public: MakeVStripes(const GRAY _p) : pattern(_p) {}
- };
- Test_image.apply(MakeVStripes(pattern));
-
- Finally, the following snippet of the verification code checks out to
- see that the pixel actions are indeed executed row-by-row. One
- iterator is used to assign to each pixel its own offset from the
- beginning of the image, and the other iterator checks it.
-
- {
- cout << "Check to see that PixelAction are executed row-wise" << endl;
- class assign_pixels : public PixelAction {
- const card test_im_nrows, test_im_ncols;
- void operation(GRAY& pixel)
- {
- assert(nrows == test_im_nrows);
- assert(ncols == test_im_ncols);
- pixel = col + row*ncols;
- }
- public: assign_pixels(const IMAGE& im) :
- test_im_nrows(im.q_nrows()), test_im_ncols(im.q_ncols()) {}
- }
- Test_image.apply(assign_pixels(Test_image));
-
- class check_pixels : public PixelPrimAction {
- GRAY curr_offset;
- void operation(GRAY& pixel) { assert(pixel == curr_offset++); }
- public: check_pixels(void) : curr_offset(0) {}
- }
- Test_image.apply(check_pixels());
- }
-
-
-
- Lazy images: instead of returning an object return a "recipe" how to
- make it. The full image would be rolled out only when and where it's
- needed:
- IMAGE map = FractalMap(order);
-
- FractalMap is a *class*, not a simple function. However similar this
- looks to a returning of an object, it's dramatically
- different. FractalMap() constructs a LazyImage, an object of just a
- few bytes long. A special "IMAGE(const LazyImage& recipe)" constructor
- follows the recipe and makes the fractal map right in place. No pixel
- is moved whatsoever!
-
- Since the FractalMap is a class, it can be subclassed to modify the
- default behavior (say, to override the default uniform noise generator
- with a gaussian noise generator, which tends to produce better looking
- clouds)
-
- class GaussNoise : public FractalMap
- {
- public:
- GaussNoise(const card order, const Seeds& seeds,
- const bits_per_pixel=8)
- : FractalMap(order,seeds,bits_per_pixel) {}
- inline int get_noise(const card scale) const {
- long sum = 0;
- for(register int i=0; i<12; i++)
- sum += rand(); // keep the result within
- return (scale * (sum-(6<<15)))>>17; } // [-scale/2,scale/2]
- };
-
- IMAGE map = type == 0 ?
- (LazyImage&)FractalMap(order,FractalMap::Seeds(sul,sll,sur,slr)) :
- (LazyImage&)GaussNoise(order,FractalMap::Seeds(sul,sll,sur,slr));
-
- This technique is particularly useful when one needs to construct an
- image in some particular way (say, by reading it from a file/database,
- or by decoding/decompressing, etc) and return it. Thus, instead of
- returning an image, one should always return a LazyImage.
-
- cout << "\tExpansion of the uniform image with a small stain\n";
- Test_image = pattern;
- Test_image(0,0) = 1;
- Test_image(1,1) = 0;
- IMAGE blown_out(IMAGE::Expand,Test_image);
- class BlowImage : public LazyImage
- {
- const IMAGE& orig_image;
- void fill_in(IMAGE& im) const
- {
- for(register int i=0; i<im.q_nrows(); i++)
- for(register int j=0; j<im.q_ncols(); j++)
- im(i,j) = orig_image(i/2,j/2);
- }
- public:
- BlowImage(const IMAGE& image) :
- LazyImage(2*image.q_nrows(),2*image.q_ncols(),image.q_depth()),
- orig_image(image) {}
- };
- IMAGE another_blown_out = BlowImage(Test_image);
- assert( another_blown_out == blown_out );
-
-
-
- Image filtration. The package supports a whole bunch of various image
- filtration techniques: convolutional, median, and morphological. They
- are *very* optimized, and *very* fast. All filtration is done
- in-place. My experience tells that it takes noticeably more time to
- read an image than to filter it. It's possible to apply a
- convolution/median filter to rows only, or to columns only, or to both
- rows and columns. A convolution kernel can have either int or
- rational coefficients; in the latter case, the denominator can be
- specified as an exact power of two, or as just any integer. Note, for
- a computer, a floating point number is the rational
- number. Convolution algorithms are optimized to all these particular
- cases (and to the generic case, too).
-
-
- Test_image = vert_line;
- verify_identity(FilterIt(Test_image).
- conv(conv_kernel(1,2,1),FilterIt::Columns)>>=2,vert_line);
- verify_identity(FilterIt(Test_image).
- conv_col(conv_kernel(1,2,3,CommonDenom(6))),vert_line);
- verify_identity(FilterIt(Test_image).
- conv(conv_kernel(0,2,0,over_2_up(1))),vert_line);
- expected = 0;
- expected.rectangle(rowcol(0,vert_line.q_ncols()/2-1),
- rowcol(vert_line.q_nrows()-1,
- vert_line.q_ncols()/2+1)) = -seed;
- verify_identity(FilterIt(Test_image=vert_line).
- conv(conv_kernel(1,1,1),FilterIt::Rows),expected);
-
- The following (intentionally contrived) snippet from the verification
- code performs a phase shift of an image through filtration
-
- expected = small_sq(0,0);
- expected.square_of(2,rowcol(sq_row-1,sq_col-1)) =
- small_sq.square_of(2,rowcol(sq_row,sq_col));
- verify_identity(FilterIt(Test_image=small_sq).
- conv(conv_kernel(0,0,2,over_2_up(1))),expected);
-
- The package supports median filtration with window sizes of three and
- five. The filtration can be done by rows only, by columns only, or by
- rows _and_ by columns. The latter is equivalent to a 2D median
- filtration with a diamond-shaped window.
-
- verify_identity(FilterIt(Test_image).median(FilterIt::RowsAndColumns,3),
- expected);
- verify_identity(FilterIt(Test_image).median(FilterIt::RowsAndColumns,5),
- expected);
-
-
- Lookup table substitutions are very powerful and very fast. Indeed, in
- a typical 512x512 8-bit deep grayscale picture, there are 1/4 M pixels
- but only 256 possible pixel values. Therefore, if one needs to perform
- some pixel calculation
- new_pixel(i,j) = f(old_pixel(i,j))
- the fastest way of accomplishing this task is to create a look-up table,
- fill it in like lookup(i) = f(i), i=0..255, and then do
- FilterIt(image).translate(lookup,LookupT::CoerceFringes);
- to substitute all "old pixels" of an 'image' with the "new_pixel"
- values. The second argument, LookupT::CoerceFringes or
- LookupT::LeaveFringes determines what to do with the pixels (if any)
- that fall outside of the lookup table range. Note, a lookup table can
- have any number of entries, even 1.
-
- The following example changes all pixels with value 2 to value 3:
-
- LookupT map(LookupT::MapTo(2,3));
- assert( FilterIt(Test_image=diverse_image).
- translate(map,LookupT::LeaveFringes) != 2 );
-
- (the assert statement makes sure that there is no pixel with value 2
- left in the translated image).
-
- a slight modification
- verify_pixel_value(FilterIt(Test_image=diverse_image).
- translate(map,LookupT::CoerceFringes),3);
-
- makes all pixels have the same value, three. Aren't lookup tables
- powerful, or what?
-
- There are many ways to create lookup tables: allocate a blank table
- and fill it in, create an Identity lookup table for a particular image
- depth and modify a few entries, or perform a composition of existing
- lookup tables.
-
- Note
- FilterIt(image).translate(lookup,LookupT::CoerceFringes);
- returns a reference to an image after the substitution, so one can
- use it in chains like
- assert( FilterIt(Test_image=diverse_image).
- translate(map,LookupT::LeaveFringes) != seed );
-
-
- ***** Grand plans
-
- - a PixelAction-like class for lookup table elements
- - in PixelAction, a method 'operation(GRAY& pixel)' should return bool
- (if it returns false, the traversal is terminated). Maybe there should
- be another class, like PixelControlledAction (which also would have
- specifications as to where to start and where/when to end the traversal),
- OR traversal classes for Rectangles
- - make operation() method of PixelAction non-virtual, and make IMAGE::apply()
- templated to a PixelAction-like class (when gcc starts supporting member
- function templates)
- - make IMAGE::allocate() virtual, or define a class ImageData that contains
- only the pixel matrix without indices. So when one needs to handle an
- array of images (frames or color-separation planes), the memory won't be
- wasted on many (mostly redundant) indices
- - add spans (by generalizing rowcol's), 1D, 2D spans, maybe even regions (non-
- rectangular spans). Add IMAGE constructors from spans. Use spans
- as generalized indices to take slices of an image
- - add a LazyMask to achieve
- B(A==5) = 4;
- (that is, for all i,j; if(A(i,j) ==5) B(i,j) = 4; ), like in Matlab.
- - simple color imagery with transformations among various tristimulus
- representations
- - support addition of a rectangle to an image or an image to a rectangle,
- or comparison between a rectangle and an image
- - add convolutions with a 5-point kernel and with 3x3 (non-separable) kernels
- - add a "non-deterministic averaging" filtering (like in LIFE, only
- survival of a pixel is probabilistic, and the chance of survival depends
- on how much a pixel sticks out of its neighborhood)
-
-
- ***** Revision history
-
- version 2.0
- Makefile is much more user-friendly.
- Introduced card (typedef'ed as unsigned int) for row and col dimensions,
- indices and other quantities which are always non-negative.
- Added PixelAction and PixelPrimAction, classes to do a specific (maybe
- very complex) operation on every pixel regardless of its position
- (PixelPrimAction) or with regard to its position (PixelAction). The image
- is traversed row-by-row.
- Introduced LazyImage (which is what to return instead of IMAGE).
- bool is used when appropriate.
- Removed GNU extensions, code is made very portable.
- Sundry of optimizations.
- IMAGE::operator(): now it returns GRAY (the pixel itself) in const contexts,
- otherwise, it returns a reference to a pixel.
- read_xwd/read_pgm use "new style" with the PixelAction stuff.
- Median filtration re-worked; can be done now only by rows, only by cols,
- and both by Rows and columns. It's heavily optimized and must be very fast.
- Added convolutions: by rows, by cols, and separable 2D. Very efficient:
- kernel coeffs can be int, rational with power2 common denom, and
- rational. The three cases are handled separately (yet uniformly) and very
- efficiently (almost as much as one can get). Filtering is done "in-place"
- Added assignments with stretching/squeezing an image to fit (IMAGE::coerce()),
- the stretching/squeezing is done by arbitrary ratio! (the sizes of the
- original and the assigned image can be anything, not necessarily int).
- Added lookup table translations.
-
- version 1.15, Feb 8, 1995
- Cosmetic changes to please gcc 2.6.3 and adjust to the new version
- of endian_io.h
-
- version 1.14, Mar 24, 1994 (previously posted on comp.sources.misc)
- Added support for reading/writing PGM and TIFF image file formats
- (in addition to the existed support for XWD format). Default write
- is in XWD format.
- Added image histogram equalization.
- Generalizing the Square_area class to the Rectangle class, which handles
- arbitrary rectangular areas of the image.
- Added comparison predicates that check a relation between an int and all
- pixels of the image.
- Added Abs() for putting down negative pixels, and application of a generic
- user-supplied function to every pixel of the image.
- Added +, -, *, >>, and << operations on rowcol (returning rowcol)
- Added assignments, comparison, and arithmetics (offsetting) on objects of
- the class rowcol.
- Added finding Extrema pixel values and image normalization for display.
-
- version 1.1, Apr 3, 1992
- Initial revision
-
-